package org.sprong.generagor.domain.entity;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.core.io.ClassPathResource;

/**
 * @author guoqiang.lv 2021/10/15
 */
public class AddressGenerator {

    private String url = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2020/";
    private boolean refresh;
    private static Map<Integer, String> cssMap = new HashMap<Integer, String>();
    private List<AddressVO> addresses = new ArrayList<>();
    private static final String hostIps = "222.74.202.234:8081,58.220.95.42:10174,222.74.202.230:8080,124.70.132.48:8081,58.220.95.8:10174,47.108.154.194:8080";
    private static List<Host> hosts = new ArrayList<>();

    private static final String[] ua = {"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36 OPR/37.0.2178.32",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586",
            "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
            "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)",
            "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)",
            "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 BIDUBrowser/8.3 Safari/537.36",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 Core/1.47.277.400 QQBrowser/9.4.7658.400",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 UBrowser/5.6.12150.8 Safari/537.36",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36 TheWorld 7",
            "Mozilla/5.0 (Windows NT 6.1; W…) Gecko/20100101 Firefox/60.0"};

    static {
        cssMap.put(1, "provincetr");
        cssMap.put(2, "citytr");
        cssMap.put(3, "countytr");
        cssMap.put(4, "towntr");
        cssMap.put(5, "villagetr");

        for (String one : hostIps.split(",")) {
            String[] split = one.split(":");
            hosts.add(new Host(split[0], Integer.parseInt(split[1])));
        }
    }

    public void parse() throws IOException, ExecutionException, InterruptedException {
        if (!refresh) {
            parseFromCache();
            if (!addresses.isEmpty()) {
                return;
            }
        }
        ThreadPoolExecutor executor = new ThreadPoolExecutor(32, 32, 0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(), r -> new Thread(r, "org.sprong.generagor.domain.entity.AddressGenerator"));
        int level = 1;
        Document connect = connect(url);
        if (Objects.isNull(connect)) {
            return;
        }
        Elements rowProvince = connect.select("tr." + cssMap.get(level));
        List<Future<List<AddressVO>>> futures = new ArrayList<>();
        for (Element provinceElement : rowProvince) {
            Elements select = provinceElement.select("a");
            for (Element province : select) {
                // 省
                String provinceName = Optional.ofNullable(province.text()).orElse("");
                futures.add(executor.submit(new ProvinceParse(province)));
            }
        }
        for (Future<List<AddressVO>> future : futures) {
            List<AddressVO> addressList = future.get();
            if (!addressList.isEmpty()) {
                addresses.addAll(addressList);
            }
        }
        addresses.forEach(System.out::println);
        if (!addresses.isEmpty()) {
            writeToCache();
        }
    }

    public List<AddressVO> getAddresses() {
        return addresses;
    }

    public AddressVO getAddress() {
        return addresses.stream().findAny().get();
    }

    private void writeToCache() throws IOException {
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "names.txt");
        BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));
        for (AddressVO address : addresses) {
            writer.write(address.getProvince() + "," + address.getCity() + "," + address.getCounty()
            + "," + address.getTown() + "," + address.getVillage());
            writer.newLine();
            writer.flush();
        }
        writer.flush();
        writer.close();

    }

    private void parseFromCache() throws IOException {
        ClassPathResource resource = new ClassPathResource("names.txt");
        if (!resource.exists()) {
            return;
        }
        InputStream resourceAsStream = resource.getInputStream();
        if (Objects.nonNull(resourceAsStream)) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream));
            String line;
            while ((line = reader.readLine()) != null) {
                // 解析地址
                String[] split = line.split(",");
                addresses.add(AddressVO.builder().province(split[0]).city(split[1]).county(split[2])
                        .town(split[3]).village(split[4]).build());
            }
        }
    }

    private static Document connect(String url) {
        if (StringUtils.isEmpty(url)) {
            return null;
        }
        Random random = new Random();
        String userAgent = ua[random.nextInt(ua.length)];
        Host host = hosts.get(random.nextInt(hosts.size()));
        while (!host.enable) {
            host = hosts.get(random.nextInt(hosts.size()));
        }
        try {
            return Jsoup.connect(url).timeout(100 * 1000).userAgent(userAgent)
                    .followRedirects(true)
//                    .proxy(host.ip, host.port)
                    .get();
        } catch (IOException e) {
            e.printStackTrace();
            if (e instanceof HttpStatusException) {
                host.enable = false;
            }
            return null;
        }
    }

    private static String parseAddr(Element element) {
        return Optional.ofNullable(element.select("td").last()).map(Element::text).orElse("");
    }

    public AddressGenerator setUrl(String url) {
        this.url = url;
        return this;
    }

    public AddressGenerator setRefresh(boolean refresh) {
        this.refresh = refresh;
        return this;
    }

    static class ProvinceParse implements Callable<List<AddressVO>> {

        private Element province;
        private List<AddressVO> list;

        ProvinceParse(Element province) {
            this.province = province;
            list = new ArrayList<>();
        }

        @Override
        public List<AddressVO> call() throws Exception {
            AddressVO addressVO = new AddressVO();
            addressVO.setProvince(Optional.ofNullable(province.text()).orElse(""));
            parseNextLevel(addressVO, province, 2);
            return list;
        }
        private void parseNextLevel(AddressVO addressVO, Element parentElement, int level) throws IOException {
            try {
                //睡眠一下，否则可能出现各种错误状态码
                Thread.sleep(100 * (new Random().nextInt(3)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Document doc = connect(parentElement.attr("abs:href"));
            if (Objects.isNull(doc)) {
                return;
            }
            Elements newsHeadlines = doc.select("tr." + cssMap.get(level));
            // 获取表格的一行数据
            for (Element element : newsHeadlines) {
                // 在递归调用的时候，这里是判断是否是村一级的数据，村一级的数据没有a标签
                Elements select = element.select("a");
                switch (level) {
                    case 1:
                        addressVO.setProvince(parseAddr(element));
                        break;
                    case 2:
                        addressVO.setCity(parseAddr(element));
                        break;
                    case 3:
                        addressVO.setCounty(parseAddr(element));
                        break;
                    case 4:
                        addressVO.setTown(parseAddr(element));
                        break;
                    case 5:
                        addressVO.setVillage(parseAddr(element));
                        break;
                    default:
                }
                if (select.size() != 0) {
                    parseNextLevel(addressVO, select.last(), level + 1);
                } else {
                    list.add(addressVO.copy());
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new AddressGenerator().parseFromCache();
    }

    static class Host {
        String ip;
        int port;
        boolean enable;

        public Host(String ip, int port) {
            this.ip = ip;
            this.port = port;
            enable = true;
        }
    }
}
